package willispace;

import entities.Builder;
import entities.Diamond;
import entities.Entity;
import entities.Factory;
import entities.Gas;
import entities.Robot;
import entities.Route;
import entities.Tile;
import entities.Supplies;
import entities.Supply;
import entities.TransportRobot;
import entities.Transporter;
import entities.Worker;
import entities.exceptions.BlockedTileException;
import entities.exceptions.MissingRouteException;
import entities.exceptions.NotEnoughSuppliesToMakeRobotException;
import entities.exceptions.NotEnoughSupplyException;
import entities.exceptions.RouteAlreadyExistsException;
import entities.exceptions.WrongFactoryException;
import entities.exceptions.WrongSupplyException;
import graphics.ResourcesManager;
import graphics.WindowManager;
import java.awt.Image;
import java.awt.Point;
import java.io.IOException;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Main WilliSpace class.
 * Manages everything.
 * @author mateusz
 */
public class WilliSpace {

    private static boolean isRunning;
    private static MapRenderer renderer;
    private static ResourcesManager resourcesManager;
    private static boolean isPaused;

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        try {
            init();
            gameLoop();
        } finally {
            //stop();
        }
    }

    /**
     * Initializes everything
     */
    private static void init() {
        Map map = null;
        addToGlobalSupplies(Config.factoryCost * 4);
        Database.updateGlobalProperties();
        WindowManager.init();
        resourcesManager = new ResourcesManager();
        try {
            map = resourcesManager.loadMap("maps/1.map");
            Database.map = map;
        } catch (IOException ex) {
            System.out.println("Can't load map file. Exit");
            stop();
        }

        renderer = new MapRenderer(map, WindowManager.fullScreenWindow.bottomPanel);

        WindowManager.controlWindow.updateDirections(0, 0);
        isRunning = true;
    }

    /**
     * Main game loop
     */
    private static void gameLoop() {

        Random randomGenerator = new Random();
        ConcurrentHashMap<Gas, Integer> explodedGases = new ConcurrentHashMap<Gas, Integer>();

        // first map draw
        renderer.draw();

        while (isRunning) {

            if (!isPaused) {
                Database.updateGlobalProperties();

                // refresh main window
                WindowManager.fullScreenWindow.validate();
                //WindowManager.fullScreenWindow.middlePanel.repaint();
                //WindowManager.fullScreenWindow.topPanel.repaint();
                //WindowManager.fullScreenWindow.repaint();

                // refresh entity window
                WindowManager.entityWindow.insidePropertiesPanel.updateLabels();
                WindowManager.entityWindow.validate();
                //WindowManager.entityWindow.propertiesPanel.repaint();

                // gas explosion
                if (randomGenerator.nextDouble() <= Config.gasExsplosionProbability) {
                    synchronized (Database.gases) {
                        int gasIndex = randomGenerator.nextInt(Database.gases.size());
                        try {
                            Gas gas = Database.gases.get(gasIndex);
                            synchronized (gas) {
                                gas.explose();
                                destroyRobotsInNeighborhood(gas);
                                explodedGases.put(gas, Config.gasExplosionTime);
                                updateMiddleLayer();
                            }
                        } catch (IndexOutOfBoundsException ex) {
                        }
                    }
                }

                // previous explosions
                for (java.util.Map.Entry<Gas, Integer> entry : explodedGases.entrySet()) {
                    Gas g = entry.getKey();
                    synchronized (g) {
                        Integer i = entry.getValue();
                        i -= 1;
                        explodedGases.put(g, i);
                        if (i == 0) {
                            explodedGases.remove(g);
                            g.quench();
                            updateMiddleLayer();
                        } else {
                            destroyRobotsInNeighborhood(g);
                        }
                    }
                }

            }

            try {
                Thread.sleep(Config.gameLoopSleepTime);
            } catch (InterruptedException ex) {
            }
        }
        System.exit(0);
    }

    /**
     * Stops all independent entities.
     * Break main game loop.
     * Should be called before exit from game.
     */
    public static void stop() {
        isRunning = false;

        destroyRobot(Database.builder);

        for (Robot r : Database.transporters) {
            destroyRobot(r);
        }

        for (Robot r : Database.workers) {
            destroyRobot(r);
        }
    }

    /**
     * Pauses game
     */
    public static void pause() {
        isPaused = !isPaused;

        // Pause all entities

        Database.builder.pause();

        for (Entity e : Database.transporters) {
            e.pause();
        }

        for (Entity e : Database.workers) {
            e.pause();
        }

        for (Entity e : Database.diamonds) {
            e.pause();
        }

        for (Entity e : Database.gases) {
            e.pause();
        }

        for (Entity e : Database.factories) {
            e.pause();
        }
    }

    /**
     * Moves map by specified coordinates
     * @param x horizontal move
     * @param y vertical move
     * @return  Direction object which specifies, what moves can be done now
     */
    public static Directions<Boolean> moveMap(int x, int y) {
        Directions<Boolean> d = null;
        if (!isPaused) {
            d = renderer.moveMap(x, y);
            // to do - change middle layer
            updateMiddleLayer();
            renderer.draw();
        }
        return d;
    }

    private static void updateMiddleLayer() {
        for (Entity e : Database.diamonds) {
            e.updateLocation();
        }

        for (Entity e : Database.factories) {
            e.updateLocation();
        }

        for (Entity e : Database.gases) {
            e.updateLocation();
        }

        WindowManager.fullScreenWindow.middlePanel.repaint();
    }

    /**
     * Check if game is paused
     * @return true, if game is paused, false otherwise
     */
    public static boolean isPaused() {
        return isPaused;
    }

    /**
     * Returns current map
     * @return current map
     */
    public static Map getMap() {
        return Database.map;
    }

    /**
     * Gets image from resources manager
     * @param name  name of image
     * @return      image with specified name
     */
    public static Image getImage(String name) {
        return resourcesManager.get(name);
    }

    // =======================
    // GLOBAL SUPPLIES ACTIONS
    // =======================
    /**
     * Try get units from global supplies.
     * If global supplies do not contains as many units, try get from some factories
     * @param units                         units to get
     * @throws NotEnoughSupplyException     if global supplies and all factories don't have as many units
     */
    public static void getFromGlobalSupplies(int units) throws NotEnoughSupplyException {
        // synchronization at static Database object
        synchronized (Database.class) {
            if (!totalGlobalSupplies(units)) {
                throw new NotEnoughSupplyException();
            }

            // units definately can be taken
            int gasUnits = 0, diamondUnits = 0;

            // try get from global global how much you can
            gasUnits += Math.min(units, Database.globalGasSupply);
            Database.globalGasSupply -= gasUnits;

            diamondUnits += Math.min(units, Database.globalDiamondSupply);
            Database.globalDiamondSupply -= diamondUnits;

            // get from factories
            while (gasUnits < units && diamondUnits < units) {
                for (Factory f : Database.factories) {
                    if (gasUnits < units) {
                        try {
                            f.getUnitsFromFactory(1, Supplies.GAS);
                            gasUnits += 1;
                        } catch (NotEnoughSupplyException e) {
                        }
                    }
                    if (diamondUnits < units) {
                        try {
                            f.getUnitsFromFactory(1, Supplies.DIAMOND);
                            diamondUnits += 1;
                        } catch (NotEnoughSupplyException e) {
                        }
                    }
                }
            }
        }
    }

    /**
     * calculates if lowerBound is lower than total global supply (including factories)
     * @param lowerBound    number of units to check
     * @return              true, if lowerBound is less than all possessed units
     */
    public static boolean totalGlobalSupplies(int lowerBound) {
        int gas = Database.globalGasSupply;
        int diamond = Database.globalDiamondSupply;

        for (Factory f : Database.factories) {
            gas += f.getGasUnits();
            diamond += f.getDiamondSupply();
        }
        return gas >= lowerBound && diamond >= lowerBound;
    }

    /**
     * Add units to global supply
     * @param units units to add
     */
    public static void addToGlobalSupplies(int units) {
        addToGlobalSupplies(units, units);
    }

    /**
     * Add units of gas and diamond to global supply
     * @param gasUnits      units of gas to add
     * @param diamondUnits  units of diamond to add
     */
    public static void addToGlobalSupplies(int gasUnits, int diamondUnits) {
        Database.globalDiamondSupply += diamondUnits;
        Database.globalGasSupply += gasUnits;
    }

    // =======================
    // ENTITIES ACTIONS
    // =======================
    // ==============
    // BUILDER
    // ==============
    /**
     * Builds gas factory
     * @param t                             tile associated with new factory
     * @throws BlockedTileException         if tile is blocked (e.g. by rock, supply)
     * @throws NotEnoughSupplyException     if supplies are not enough to build new factory
     */
    public static void buildGasFactory(Tile t) throws BlockedTileException, NotEnoughSupplyException {
        buildFactoryInit(t);
        Database.builder.buildGasFactory(t);
        // block this tile, but set unlimited
        t.block();
    }

    /**
     * Builds diamond factory
     * @param t                             tile associated with new factory
     * @throws BlockedTileException         if tile is blocked (e.g. by rock, supply)
     * @throws NotEnoughSupplyException     if supplies are not enough to build new factory
     */
    public static void buildDiamondFactory(Tile t) throws BlockedTileException, NotEnoughSupplyException {
        buildFactoryInit(t);
        Database.builder.buildDiamondFactory(t);
        // block this tile, but set unlimited
        t.block();
    }

    /**
     * Initial action before factory build
     * @param t                             tile associated with new factory
     * @throws BlockedTileException         if tile is blocked (e.g. by rock, supply)
     * @throws NotEnoughSupplyException     if supplies are not enough to build new factory
     */
    public static void buildFactoryInit(Tile t) throws NotEnoughSupplyException, BlockedTileException {
        // sprawdzanie czy dane pole nie jest
        if (!Builder.canBuildFactoryOnTile(t)) {
            throw new BlockedTileException();
        }
        // alokowanie zasobów
        getFromGlobalSupplies(Config.factoryCost);


    }

    /**
     * Build factory extracted supply
     * @param t         tile associated with new factory
     * @param supply    supply which factory will extract
     * @return          new Factory object
     * @see Factory
     */
    public static Factory buildFactory(Tile t, Supplies supply) {
        Factory f = new Factory(t, resourcesManager.get("factory"), supply, "factory" + Integer.toString(Database.factoriesCounter++));

        t.setUnlimited();

        // add to database
        Database.factories.add(f);

        // associate with entity
        t.associateWithEntity(f);

        // update layer containings factories
        updateMiddleLayer();

        return f;
    }

    /**
     * Move builder 
     * @param b                         Builder to move
     * @param t                         Tile on which builder wants to go
     * @throws BlockedTileException     if tile is blocked (e.g. by rock, supply)
     */
    public static void moveBuilder(Builder b, Tile t) throws BlockedTileException {
        b.goTo(t);
    }

    // ==============
    // FACTORY
    // ==============
    /**
     * Adds route from factory to supply
     * @param f                             factory to which route will be added
     * @param supply                        supply to connect with factory
     * @throws WrongSupplyException         if supply type and factory extracted type mismatch
     * @throws RouteAlreadyExistsException  if factory already has route to this supply
     */
    public static void addRouteFromFactoryToSupply(Factory f, Supply supply) throws WrongSupplyException, RouteAlreadyExistsException {
        f.addRouteToSupply(supply);
    }

    /**
     * Add route from factory to another factory
     * @param f1                            factory to which route will be added
     * @param f2                            factory to which route will lead 
     * @throws RouteAlreadyExistsException  if factory f1 already has route to factory f2
     * @throws WrongFactoryException        if factory f2 is factory f1
     */
    public static void addRouteFromFactoryToFactory(Factory f1, Factory f2) throws RouteAlreadyExistsException, WrongFactoryException {
        f1.addRouteToFactory(f2);
    }

    /**
     * Call factory to make new Worker
     * @param f                                         factory in which worker will be created
     * @throws MissingRouteException                    if factory hasn't route to supply or route to another factory
     * @throws NotEnoughSuppliesToMakeRobotException    if factory hasn't enough supplies to create new worker
     */
    public static void factoryMakeWorker(Factory f) throws MissingRouteException, NotEnoughSuppliesToMakeRobotException {
        f.newWorker();
    }

    /**
     * Call factory to make new Transporter
     * @param f                                         factory in which transporter will be created
     * @throws MissingRouteException                    if factory hasn't route to supply or route to another factory
     * @throws NotEnoughSuppliesToMakeRobotException    if factory hasn't enough supplies to create new transporter
     */
    public static void factoryMakeTransporter(Factory f) throws MissingRouteException, NotEnoughSuppliesToMakeRobotException {
        f.newTransporter();
    }

    /**
     * Changes workers supply
     * @param worker                    worker who wants to change supply
     * @param supply                    new supply
     * @throws MissingRouteException    if factory-mother of this robot hasn't route to this supply
     * @throws WrongSupplyException     if supply type mismatches worker extracted type
     */
    public static void changeWorkerRoute(Worker worker, Supply supply) throws MissingRouteException, WrongSupplyException {
        Factory f = worker.getFactory();
        f.changeWorkerRoute(worker, supply);
    }

    /**
     * Changes next transporter target
     * @param transporter               transporter who wants new next target
     * @param factory                   next target
     * @throws MissingRouteException    if factory-mother of this robot hasn't route to this factory
     */
    public static void addTransporterTarget(Transporter transporter, Factory factory) throws MissingRouteException {
        Factory transporterFactory = transporter.getFactory();
        transporterFactory.addTransporterTarget(transporter, factory);
    }

    /**
     * Destroys factory
     * Causes all robots created by this factory to be destroyed
     * All units designed to build this factory and its robots, go back to global supplies
     * @param factory factory to destroy
     */
    public static void destroyFactory(Factory factory) {
        factory.destroy();
        
        // try to delete routes to this factory from all others
        for(Factory f: Database.factories){
            try {
                f.deleteRouteToFactory(factory);
            } catch (MissingRouteException ex) {
            }
        }
        
        Database.factories.remove(factory);
        
        addToGlobalSupplies(Config.factoryCost);
    }

    // ==============
    // ROBOT
    // ==============
    /**
     * Destroys robot
     * All units designed to build this robot, go back to global supplies
     * @param r robot to destroy
     * @see Robot
     */
    public static void destroyRobot(Robot r) {
        Thread t;
        // build cost
        int units = 0;

        // kill robot
        t = r.getThread();
        units = r.kill();

        try {
            // wait for its thread end
            t.join();
        } catch (InterruptedException ex) {
            Logger.getLogger(WilliSpace.class.getName()).log(Level.SEVERE, null, ex);
        }

        // remove from database
        if (r instanceof Worker) {
            Database.workers.remove((Worker) r);
        } else if (r instanceof Transporter) {
            Database.transporters.remove((Transporter) r);
        }

        // return units to global supplies
        addToGlobalSupplies(units);
    }

    /**
     * Wakes up all robots.
     * Causes all robots to update their localizations.
     * @see Robot
     */
    public synchronized static void notifyAllRobots() {
        // run later
        java.awt.EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {
                notifyAllWorkers();
                notifyAllTransporters();
                Database.builder.wakeUp();
            }
        });

    }

    // ========
    // WORKER
    // ========
    /**
     * Creates new Worker
     * @param factory   factory-mother that should create new worker
     * @return          new Worker instance
     * @see Worker
     */
    public static Worker makeWorker(Factory factory) {
        Worker w = new Worker(factory, resourcesManager.get("worker"), "worker" + Integer.toString(Database.workersCounter++));
        Database.workers.add(w);
        return w;
    }

    /**
     * Wakes up all workers
     * @see #notifyAllRobots() 
     */
    public static void notifyAllWorkers() {
        for (Worker w : Database.workers) {
            w.wakeUp();
        }
    }

    // ========
    // TRANSPORTER
    // ========
    /**
     * Creates new transporter instance
     * @param factory   factory-mother that should create new worker
     * @param route     route that will be first track of new transporter
     * @return          new transporter instance
     */
    public static Transporter makeTransporter(Factory factory, Route route) {
        Transporter t = new Transporter(factory, resourcesManager.get("transporter"), route, "transporter" + Integer.toString(Database.transportersCounter++));
        Database.transporters.add(t);
        return t;
    }

    /**
     * Wakes up all transporters
     * @see #notifyAllRobots() 
     */
    public static void notifyAllTransporters() {
        for (Transporter t : Database.transporters) {
            t.wakeUp();
        }
    }

    // ==============
    // SUPPLY
    // ==============
    /**
     * Removes supply
     * @param s supply to be removed
     */
    public static void removeSupply(Supply s) {
        // remove supply from proper database
        if (s.getType() == Supplies.DIAMOND) {
            Database.diamonds.remove((Diamond) s);
        } else if (s.getType() == Supplies.GAS) {
            Database.gases.remove((Gas) s);
        }

        // remove routes to this supply from all factories
        for (Factory f : Database.factories) {
            try {
                f.deleteRouteToSupply(s);
            } catch (MissingRouteException ex) {
            }
        }

        s.destroy();

        // middle layer update
        updateMiddleLayer();

    }

    // after gas explosion
    private static void destroyRobotsInNeighborhood(Gas gas) {
        Point gasPosition = gas.getXY();

        synchronized (Database.workers) {
            int size = Database.workers.size()-1;
            for (int i = size;i>=0;i--) {
                Worker w = Database.workers.get(i);
                if (inRange(w.getXY(), gasPosition, Config.tileWidth * Config.gasExplosionRange)) {
                    destroyRobot(w);
                }
            }
        }
        synchronized (Database.transporters) {
            int size = Database.transporters.size()-1;
            for (int i=size;i>=0;i--) {
                Transporter t = Database.transporters.get(i);
                if (inRange(t.getXY(), gasPosition, Config.tileWidth * Config.gasExplosionRange)) {
                    destroyRobot(t);
                }
            }
        }
    }

    private static boolean inRange(Point p1, Point p2, int range) {
        return Math.abs(p1.x - p2.x) < range && Math.abs(p1.y - p2.y) < range;
    }
}
